//
//  GeoLocationService.swift
//  EarbudsCommon
//
//  Created by 李然 on 2024/5/8.
//

import Foundation
import CoreLocation
import RxSwift
import RxCocoa

public class GeoLocationService {
    public static let instance = GeoLocationService()
    
    public private (set) var authorized: Driver<Bool>
    public private (set) var location: Driver<CLLocation>
    public private (set) var heading: Driver<CLHeading>
    
    private let manager = CLLocationManager()

    public init() {
        manager.distanceFilter = kCLDistanceFilterNone
        manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        
        authorized = Observable.deferred({ [unowned manager] () -> Observable<(CLLocationManager, CLAuthorizationStatus)> in
            let status = CLLocationManager.authorizationStatus()
            return manager.rx.didChangeAuthorization.startWith((manager, status))
        }).asDriver(onErrorJustReturn: (manager, CLAuthorizationStatus.notDetermined)).map({
            switch $0.1 {
            case .authorizedAlways:
                return true
            case .authorizedWhenInUse:
                return true
            default:
                return false
            }
        })
        
        location = manager.rx.didUpdateLocations
            .debounce(.seconds(1), scheduler: MainScheduler.instance)
            .asDriver(onErrorJustReturn: (manager, [CLLocation]())).flatMap({
                return $0.1.last.map(Driver.just) ?? Driver.empty()
            })
        
        heading = manager.rx.didUpdateHeading.asDriver(onErrorJustReturn: (manager, CLHeading())).map { $0.1 }
        
        manager.requestAlwaysAuthorization()
    }
    
    func getLocation() -> Observable<CLLocation?> {
        return self.authorized.asObservable().flatMapLatest { authoried -> Observable<CLLocation?> in
            if authoried {
                self.manager.startUpdatingLocation()
                return self.manager.rx.didUpdateLocations
                    .debounce(.seconds(1), scheduler: MainScheduler.instance)
                    .flatMap {
                        self.manager.stopUpdatingLocation()
                        return Observable.just($0.1.last)
                    }
            } else {
                return Observable.just(nil)
            }
        }
    }
    
    public func startUpdatingLocation() {
        manager.startUpdatingLocation()
    }
    
    public func stopUpdatingLocation() {
        manager.stopUpdatingLocation()
    }
    
    public func startUpdatingHeading() {
        manager.startUpdatingHeading()
    }
    
    public func stopUpdatingHeading() {
        manager.stopUpdatingHeading()
    }
}

extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}

class CLLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
    
    init(manager: CLLocationManager) {
        super.init(parentObject: manager, delegateProxy: CLLocationManagerDelegateProxy.self)
    }
    static func registerKnownImplementations() {
        self.register { CLLocationManagerDelegateProxy(manager: $0) }
    }
    
    internal lazy var didUpdateLocationsSubject = PublishSubject<(CLLocationManager, [CLLocation])>()
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        forwardToDelegate()?.locationManager?(manager, didUpdateLocations: locations)
        didUpdateLocationsSubject.onNext((manager, locations))
    }
    
    internal lazy var didUpdateHeadingSubject = PublishSubject<(CLLocationManager, CLHeading)>()
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        forwardToDelegate()?.locationManager?(manager, didUpdateHeading: newHeading)
        didUpdateHeadingSubject.onNext((manager, newHeading))
    }
    
    deinit {
        didUpdateLocationsSubject.onCompleted()
        didUpdateHeadingSubject.onCompleted()
    }
}

extension Reactive where Base: CLLocationManager {
    
    var delegate: CLLocationManagerDelegateProxy {
        return CLLocationManagerDelegateProxy.proxy(for: base)
    }
    
    var didUpdateHeading: Observable<(CLLocationManager, CLHeading)> {
        return delegate.didUpdateHeadingSubject
    }
    
    var didUpdateLocations: Observable<(CLLocationManager, [CLLocation])> {
        return delegate.didUpdateLocationsSubject
    }
    
    var didChangeAuthorization: Observable<(CLLocationManager, CLAuthorizationStatus)> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)))
            .map { (a) -> (CLLocationManager, CLAuthorizationStatus) in
                let manager = try castOrThrow(resultType: CLLocationManager.self, object: a[0])
                let number = try castOrThrow(resultType: NSNumber.self, object: a[1])
                let status = CLAuthorizationStatus(rawValue: number.int32Value)!
                
                return (manager, status)
            }
    }
}

private func castOrThrow<T>(resultType: T.Type, object: Any) throws -> T {
    guard let resultValue = object as? T else {
        throw RxCocoaError.castingError(object: object, targetType: resultType)
    }
    
    return resultValue
}
